/*****************************************************************************
 * Copyright (C) Stacy Curl. All rights reserved.                            *
 * ------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the BSD      *
 * style license a copy of which has been included with this distribution in *
 * the LICENSE.txt file.                                                     *
 *****************************************************************************/
package picounit.features.base;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import picounit.PicoUnitException;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class ProxyFactory {
	private final ConstructorUtil constructorUtil;
	private final Map<Class<?>, Class<?>> proxyClassMap = new HashMap<Class<?>, Class<?>>();

	public ProxyFactory() {
		this.constructorUtil = new ConstructorUtil(new DefaultStubFactory(this));
	}

	public static boolean isProxy(Object object) {
		return object != null && isProxy(object.getClass());
	}
	
	public Class getProxiedClass(Class proxyClass) {
		return proxyClassMap.get(proxyClass);
	}

	private static boolean isProxy(Class<?> clazz) {
		return clazz.isInterface() ? Proxy.isProxyClass(clazz) : Enhancer.isEnhanced(clazz);
	}

	public <T> T create(Class<T> clazz, MethodInterceptor methodInterceptor) {
		if (clazz.isInterface()) {
			return createInterfaceProxy(clazz, convertMethodInterceptor(methodInterceptor));
		}
		else {
			return createClassProxy(clazz, methodInterceptor);
		}
	}

	@SuppressWarnings("unchecked")
	public <T> T create(Class<T> clazz, InvocationHandler invocationHandler) {
		if (isProxy(clazz)) {
			System.err.println("proxying a proxy is deprecated; " + clazz.getName());
			return (T) create(clazz.getSuperclass(), invocationHandler);
		}

		if (clazz.isInterface()) {
			return createInterfaceProxy(clazz, invocationHandler);
		}
		else {
			return createClassProxy(clazz, convertInvocationHandler(invocationHandler));
		}
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Implementation methods
	///////////////////////////////////////////////////////////////////////////////////////////////

	@SuppressWarnings("unchecked")
	private <T> T createInterfaceProxy(final Class<T> clazz, final InvocationHandler invocationHandler) {
		ClassLoader classLoader = clazz.getClassLoader();
		
//		System.out.println("Creating proxy for: " + clazz.getName() + 
//			"\nwith classLoader: " + classLoader);

		T newProxyInstance = (T) Proxy.newProxyInstance(classLoader, new Class[] {clazz}, invocationHandler);
		
//		System.out.println("Result has classLoader: " +
//			newProxyInstance.getClass().getClassLoader());

		proxyClassMap.put(newProxyInstance.getClass(), clazz);
			
		return newProxyInstance;
	}
	
	@SuppressWarnings({"unchecked", "unused"})
	private <T> T createInterfaceProxy_new(Class<T> clazz, InvocationHandler invocationHandler) {
		Enhancer enhancer = new Enhancer();
		enhancer.setCallback(convertInvocationHandler(invocationHandler));
		enhancer.setInterfaces(new Class[] {clazz});
		return (T) enhancer.create();
	}

	@SuppressWarnings("unchecked")
	private <T> T createClassProxy(Class<T> clazz, MethodInterceptor callback) {
		Enhancer enhancer = new Enhancer();

		enhancer.setCallback(callback);

		enhancer.setSuperclass(clazz);

		Class[] constructorTypes = getConstructorTypes(clazz);

		T create = (T) enhancer.create(constructorTypes, getConstructorValues(clazz, constructorTypes));
		
//		System.out.println("Create class proxy for: " + clazz.getName() +
//			"\nwith classLoader: " + clazz.getClassLoader());
//		
//		System.out.println("Result has classLoader: " + create.getClass().getClassLoader());
		
		return create;
	}

	private MethodInterceptor convertInvocationHandler(final InvocationHandler invocationHandler) {
		return new MethodInterceptor() {
			public Object intercept(Object proxy, Method method, Object[] args,
				MethodProxy methodProxy) throws Throwable {
				return invocationHandler.invoke(proxy, method, args);
			}
		};
	}

	private InvocationHandler convertMethodInterceptor(final MethodInterceptor methodInterceptor) {
		return new InvocationHandler() {
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				return methodInterceptor.intercept(methodInterceptor, method, args, null);
			}
		};
	}

	private Class<?>[] getConstructorTypes(Class<?> clazz) {
		return constructorUtil.getConstructorToUse(clazz).getParameterTypes();
	}

	private Object[] getConstructorValues(Class<?> toConstruct, Class<?>[] constructorTypes) {
		try {
			return constructorUtil.getArgsForTypes(toConstruct, constructorTypes);
		}
		catch (IllegalArgumentException illegalArgumentException) {
			System.err.println("Unable to getConstructorValues for: " + toConstruct.getName());
			System.err.println(new HashSet<Class<?>>(Arrays.asList(constructorTypes)));
			illegalArgumentException.printStackTrace();

			throw new PicoUnitException(illegalArgumentException);
		}
		catch (InstantiationException instantiationException) {
			throw new PicoUnitException(instantiationException);
		}
		catch (IllegalAccessException illegalAccessException) {
			throw new PicoUnitException(illegalAccessException);
		}
		catch (InvocationTargetException invocationTargetException) {
			throw new PicoUnitException(invocationTargetException);
		}
	}
}
